package gitee.derumin.lib_for_derumin.logger;

import com.mojang.logging.LogUtils;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerManager;
import net.minecraft.text.Text;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

import static gitee.derumin.lib_for_derumin.Lib_for_derumin.CONFIG;

public class DeruminLogger {
    private static final Logger LOGGER = LogUtils.getLogger();
    private static final DeruminLogger INSTANCE = new DeruminLogger();
    private MinecraftServer server;
    private final List<String> logs;
    private final List<String> oldLogs;

    private DeruminLogger(){
        this.server = null;
        this.logs = new ArrayList<>();
        this.oldLogs = new ArrayList<>();
    }

    public static void init(){
        ServerLifecycleEvents.SERVER_STARTING.register(server1 -> INSTANCE.server = server1);
        ServerLifecycleEvents.SERVER_STOPPING.register(server -> INSTANCE.saveLogsImmediately(null, true));
    }

    public static DeruminLogger getLogger(){
        return INSTANCE;
    }

    public static String getFormattedTime(){
        return "[" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "]";
    }

    public static String getFormattedTimeForFileName(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH-mm-ss"));
    }

    /**
     * 这会强制关闭服务器！仅供会导致致命错误的情形使用。
     */
    public void fatal(String fatal){
        addLog("[FATAL ERROR]" + fatal);
        this.server.stop(false);
    }

    public void error(String error, AnnounceType type){
        LOGGER.error(error);
        String log = "[ERROR]" + error;
        addLogAndAnnounce(log, type);
    }

    public void warn(String warn, AnnounceType type){
        LOGGER.warn(warn);
        String log = "[WARN]" + warn;
        addLogAndAnnounce(log, type);
    }

    public void info(String info, AnnounceType type){
        LOGGER.info(info);
        String log = "[INFO]" + info;
        addLogAndAnnounce(log, type);
    }

    public void record(String record){
        addLog("[RECORD]" + record);
    }

    private void addLogAndAnnounce(String announcement, AnnounceType type){
        addLog(announcement);
        if (this.server != null) {
            switch (type) {
                case ALL -> this.server.getPlayerManager().getPlayerList().forEach(player
                        -> player.sendMessageToClient(Text.of(announcement), false));
                case OPS -> {
                    PlayerManager playerManager = this.server.getPlayerManager();
                    playerManager.getPlayerList().forEach(player -> {
                        if (playerManager.isOperator(player.getGameProfile())) {
                            player.sendMessageToClient(Text.of(announcement), false);
                        }
                    });
                }
                case NONE -> {
                }
            }
        }
    }

    private void addLog(String log){
        this.logs.add(getFormattedTime() + (this.server == null? "(Before Server Started)" : "") + log);
        checkAndSave();
    }

    private void checkAndSave(){
        if (this.logs.size() >= CONFIG.maxLogsQuantityInAFile){
            saveLogsImmediately(null, true);
        }
    }

    public void saveLogsImmediately(@Nullable String fileName, Boolean clear){
        String fileName1 = fileName == null? getFormattedTimeForFileName() : fileName;
        List<String> toBeSaved = new ArrayList<>(this.logs);
        new Thread(() -> saveLogs(fileName1, toBeSaved)).start();
        if (clear) {
            this.oldLogs.addAll(this.logs);
            this.logs.clear();
            int j = CONFIG.maxLogsQuantityInAFile;
            for (int i = this.oldLogs.size(); i > j; i--) {
                this.oldLogs.remove(0);
            }
        }
    }

    private void saveLogs(String fileName, List<String> toBeSaved){
        Path logPath = CONFIG.logDirectoryPath;
        if (logPath == null){
            logPath = FabricLoader.getInstance().getGameDir().resolve("deruminlogs");
        }
        File file = logPath.toFile();
        if (file.exists() || file.mkdirs()) {
            try {
                File file1 = logPath.resolve(fileName + ".log").toFile();
                if (file1.exists() && !file1.delete()) {
                    error("[DeruminLogger]Failed to recover eponymous file: " + file1.getName() + "!", AnnounceType.OPS);
                } else {
                    try {
                        if (file1.createNewFile()) {
                            try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file1, false))) {
                                for (String s : toBeSaved) {
                                    bufferedWriter.write(s);
                                    bufferedWriter.newLine();
                                }
                            }
                            info("[DeruminLogger]Logs saved.", AnnounceType.OPS);
                            limitLogStorage();
                        }
                    } catch (IOException e) {
                        error("[DeruminLogger]Failed to save logs!!", AnnounceType.OPS);
                    }
                }
            } catch (InvalidPathException e) {
                error("[DeruminLogger]The file name '" + fileName + "' is not valid!", AnnounceType.OPS);
            }
        }
    }

    public void limitLogStorage(){
        Path logPath = CONFIG.logDirectoryPath;
        if (logPath == null){
            logPath = FabricLoader.getInstance().getGameDir().resolve("deruminlogs");
        }
        File file = logPath.toFile();
        if (file.exists() && file.isDirectory()){
            File[] files = file.listFiles();
            if (files != null){
                List<File> listedFiles = new ArrayList<>();
                for (File file1 : files){
                    if (file1.isFile() && file1.getName().endsWith(".log")){
                        listedFiles.add(file1);
                    }
                }
                int maxQuantity = CONFIG.maxLogFilesQuantity;
                if (listedFiles.size() > maxQuantity){
                    listedFiles.sort(((o1, o2) -> o2.getName().compareTo(o1.getName())));
                    while (listedFiles.size() > maxQuantity){
                        listedFiles.remove(listedFiles.size() -1);
                    }
                }
            }
        }
    }

    public List<String> getRecentLogs(int quantity){
        List<String > result = new ArrayList<>();
        int lq = this.logs.size();
        if (quantity <= lq){
            int i = lq - quantity;
            while (i < lq){
                result.add(this.logs.get(i));
                i++;
            }
        }else {
            int oq = this.oldLogs.size();
            int j = Math.max(oq + lq - quantity, 0);
            while (j < oq){
                result.add(this.oldLogs.get(j));
                j++;
            }
            result.addAll(this.logs);
        }
        return result;
    }

    public enum AnnounceType {
        NONE,
        OPS,
        ALL;

    }

}
